In the last class we learnt how to enclose a certain block of code inside a function. Now an important question arises. How frequently should we do this ?
A function extracts a computation out of your code, encapsulates it in a black box labeled by a name. Writing a function is like defining the meaning of a word before it is used in writing. Even if that word is used only once in your story you should surely write out it's definition if you think that makes writing more understandable.
Modular coding is a software design technique which translates to decomposing the functionality of a larger program into independent parts. Each of those parts should more or less implement one aspect of the larger requisite functionality.
In other words, you're implementing the divide-and-conquer paradigm at the method level. Other divide-and-conquer you may be witnessing in Algorithmic Thinking, but let's leave that for now.
If you visit a Bank to do an operation like a fund transfer then you'll have to interact with multiple entities such as the bank teller, clerk, account manager, maybe the ATM as well, etc. You get the point, right?
The important thing to notice is that the task of "fund transfer" is being divided into smaller subtasks which are being performed by different people. The collective subresults of all those people involved get added up which finally results in a successful transfer.
Consider the following code :
In [ ]:
from math import sqrt
import random
line_segments = [((random.uniform(0, 1), random.uniform(0, 1)),
(random.uniform(0, 1), random.uniform(0, 1)))
for i in range(0, 1000)]
positive_slope = len([segment
for segment in line_segments
if (segment[1][1] - segment[0][1])/(segment[1][0] - segment[0][0]) > 0
])
print("Line segments with positive slope : ", positive_slope)
Now let's make it modular :
In [ ]:
from math import sqrt
import random as r
def generate_point(x_range, y_range):
return (r.uniform(*x_range), r.uniform(*y_range))
def slope(segment):
if segment[1][0] == segment[0][0]:
y_diff = segment[1][1] - segment[0][1]
return float('inf') * y_diff if y_diff != 0 else 0
else:
return (segment[1][1] - segment[0][1])/(segment[1][0] - segment[0][0])
def generate_line_segments(x_range, y_range, count):
return [
(generate_point(x_range, y_range), generate_point(x_range, y_range))
for i in range(0, count)
]
def get_positive_sloped_segments(line_segments):
return [
segment
for segment in line_segments
if slope(segment) > 0
]
x_range = (0, 1)
y_range = (0, 1)
line_segments = generate_line_segments(x_range, y_range, 1000)
positive_line_segments = get_positive_sloped_segments(line_segments)
print("Line segments with positive slope : ", len(positive_line_segments))
So, why is the above code better than the previous one in-spite of similar client level functionality ?
Efficient Program Development/Debugging : Say if you had to re-design a certain aspect of the main functionality it's easier to do so in modular code. This is because that aspect would be referenced by a function name and if the code is well designed you would need to understand ONLY that function in order to improve it.
Re-use of the constituent modules : If you needed to compute the slope of a line segment in some other program the slope method defined here can be re-used again.
In [ ]:
from math import sqrt
import random as r
def generate_point(x_range, y_range):
return (r.uniform(*x_range), r.uniform(*y_range))
def compute_direction_cosines(segment):
dim = len(segment[0])
diff_between_points = [segment[1][i] - segment[0][i]
for i in range(0, dim)]
distance_between_points = sqrt(sum([diff_between_points[i] ** 2
for i in range(0, dim)]))
return [i / distance_between_points
for i in diff_between_points]
def is_positive_slope(segment):
dir_cosines = compute_direction_cosines(segment)
# 0 is positive, 1 is negative sign
is_positive = all([i >= 0 for i in dir_cosines]) or all([i < 0 for i in dir_cosines])
return is_positive
def generate_line_segments(count, x_range, y_range):
return [
(generate_point(x_range, y_range), generate_point(x_range, y_range))
for i in range(0, count)
]
def get_positive_sloped_segments(line_segments):
return [
segment
for segment in line_segments
if is_positive_slope(segment)
]
x_range = (0, 1)
y_range = (0, 1)
line_segments = generate_line_segments(1000, x_range, y_range)
positive_line_segments = get_positive_sloped_segments(line_segments)
print("Line segments with positive slope : ", len(positive_line_segments))
Is it better than the previous ones ?
Yes it is....Why though ?
When writing modular code, one needs to keep in mind two models of software development:
1. Scalability
2. Robustness.
Robustness means that your code can deal with incorrect input and runtime errors. This we haven't covered as of yet. Scalability means that your code should scale decently with increasing amount of work. Now this can be interpreted in multiple ways. It can mean than when amount of input increases then code should also scale correctly with it (i.e. not take unexpected amount of time). It can also mean that adding new features which depend on most/all of the submodules shouldn't be too much difficult.
So, in the above code we can see the second characteristic of scalability in action.
In [ ]:
from math import sqrt
import random as r
def generate_point(x_range, y_range, z_range=None):
if z_range is None:
return (r.uniform(*x_range), r.uniform(*y_range))
return (r.uniform(*x_range), r.uniform(*y_range), r.uniform(*z_range))
def compute_direction_cosines(segment):
# No change at all !
dim = len(segment[0])
diff_between_points = [segment[1][i] - segment[0][i]
for i in range(0, dim)]
distance_between_points = sqrt(sum([diff_between_points[i] ** 2
for i in range(0, dim)]))
return [i / distance_between_points
for i in diff_between_points]
def is_positive_slope(segment):
# No change at all !
dir_cosines = compute_direction_cosines(segment)
# 0 is positive, 1 is negative sign
is_positive = all([i >= 0 for i in dir_cosines]) or all([i < 0 for i in dir_cosines])
return is_positive
def generate_line_segments(count, x_range, y_range, z_range=None):
return [
(generate_point(x_range, y_range, z_range),
generate_point(x_range, y_range, z_range))
for i in range(0, count)
]
def get_positive_sloped_segments(line_segments):
# No change at all !
return [
segment
for segment in line_segments
if is_positive_slope(segment)
]
x_range = (0, 1)
y_range = (0, 1)
z_range = (0, 1)
line_segments = generate_line_segments(1000, x_range, y_range, z_range)
positive_line_segments = get_positive_sloped_segments(line_segments)
print("Line segments with positive slope : ", len(positive_line_segments))
How to apply the divide and conquer paradigm effectively is something which can only be achieved through practice. Please do go through the practice problems on the repository and more importantly do the mini-project for the next week as well as you can.
Object-oriented programming (OOP) refers to a software design practice in which the programmer defines both data type of a data structure as well as methods that applied to it.
Consider a character in a video game(say Skyrim). That character you control as well as other non-playable ones in the game are internally represented by objects. When you strike a wild wolf in the game with your sword you are actually interacting with that character and affecting one of it's attributes, namely it's health.
So, you can say that an object is an entity which has a current state described by some attributes and can perform some methods.
Attributes : An attribute of an object is something that partly or wholly describes it's state.
Methods : Methods of an object are the actions which the object can perform.
Let us consider a living organism such as the simplest cell.
Attributes :
Methods :
From next lecture we will begin to transform our abstract ideas about objects, their attributes and methods into concrete code.
In [ ]:
while expression:
execute statement(s)
In [2]:
i = 5
while i > 0:
print(i, end=" : ")
i -= 1
In most languages a do-while loop means that the loop body will be executed at least once. This is because the body is executed first and then the test condition is checked. Python doesn't natively have do-while loop. However it can be implemented in a certain fashion.
In [ ]:
while True:
execute statement(s)
if fail_condition:
break